/**
 * @copyright https://github.com/nodejs/node/blob/master/lib/events.js
 */
/* eslint-disable */
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var objectCreate = Object.create || objectCreatePolyfill;
var objectKeys = Object.keys || objectKeysPolyfill;
var bind = Function.prototype.bind || functionBindPolyfill;

// By default Dispatchers will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
let defaultMaxListeners = 10;

export default class EventEmitter {
	static listenerCount(emitter, type) {
		if (typeof emitter.listenerCount === 'function') {
			return emitter.listenerCount(type);
		} else {
			return listenerCount.call(emitter, type);
		}
	}

	constructor() {
		if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) {
			this._events = objectCreate(null);
			this._eventsCount = 0;
		}

		this._maxListeners = this._maxListeners || undefined;
	}

	// // Obviously not all Emitters should be limited to 10. This function allows
	// // that to be increased. Set to zero for unlimited.
	// setMaxListeners(n) {
	//     if (typeof n !== 'number' || n < 0 || isNaN(n))
	//         throw new TypeError('"n" argument must be a positive number');
	//     this._maxListeners = n;
	//     return this;
	// }
	//
	// getMaxListeners() {
	//     return $getMaxListeners(this);
	// }

	emit(type) {
		var er, handler, len, args, i, events;
		var doError = type === 'error';

		events = this._events;
		if (events) doError = doError && events.error == null;
		else if (!doError) return false;

		// If there is no 'error' event listener then throw.
		if (doError) {
			if (arguments.length > 1) er = arguments[1];
			if (er instanceof Error) {
				throw er; // Unhandled 'error' event
			} else {
				// At least give some kind of context to the user
				var err = new Error('Unhandled "error" event. (' + er + ')');
				err.context = er;
				throw err;
			}
		}

		handler = events[type];

		if (!handler) return false;

		var isFn = typeof handler === 'function';
		len = arguments.length;
		switch (len) {
			// fast cases
			case 1:
				emitNone(handler, isFn, this);
				break;
			case 2:
				emitOne(handler, isFn, this, arguments[1]);
				break;
			case 3:
				emitTwo(handler, isFn, this, arguments[1], arguments[2]);
				break;
			case 4:
				emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
				break;
			// slower
			default:
				args = new Array(len - 1);
				for (i = 1; i < len; i++) args[i - 1] = arguments[i];
				emitMany(handler, isFn, this, args);
		}

		return true;
	}

	on(type, listener) {
		return _addListener(this, type, listener, false);
	}

	once(type, listener) {
		if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function');
		this.on(type, _onceWrap(this, type, listener));
		return this;
	}

	off(type, listener) {
		return _removeListener.call(this, type, listener);
	}

	removeAllListeners(type) {
		var listeners, events, i;

		events = this._events;
		if (!events) return this;

		// not listening for off, no need to emit
		if (!events.off) {
			if (arguments.length === 0) {
				this._events = objectCreate(null);
				this._eventsCount = 0;
			} else if (events[type]) {
				if (--this._eventsCount === 0) this._events = objectCreate(null);
				else delete events[type];
			}
			return this;
		}

		// emit off for all listeners on all events
		if (arguments.length === 0) {
			var keys = objectKeys(events);
			var key;
			for (i = 0; i < keys.length; ++i) {
				key = keys[i];
				if (key === 'off') continue;
				this.removeAllListeners(key);
			}
			this.removeAllListeners('off');
			this._events = objectCreate(null);
			this._eventsCount = 0;
			return this;
		}

		listeners = events[type];

		if (typeof listeners === 'function') {
			this.off(type, listeners);
		} else if (listeners) {
			// LIFO order
			for (i = listeners.length - 1; i >= 0; i--) {
				this.off(type, listeners[i]);
			}
		}

		return this;
	}

	listeners(type) {
		return _listeners(this, type, true);
	}

	rawListeners(type) {
		return _listeners(this, type, false);
	}

	listenerCount() {
		return EventEmitter.listenerCount.apply(this, arguments);
	}
}

let hasDefineProperty;
try {
	var o = {};
	if (Object.defineProperty) Object.defineProperty(o, 'x', {value: 0});
	hasDefineProperty = o.x === 0;
} catch (err) {
	hasDefineProperty = false;
}
if (hasDefineProperty) {
	Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
		enumerable: true,
		get: function() {
			return defaultMaxListeners;
		},
		set: function(arg) {
			// check whether the input is a positive number (whose value is zero or
			// greater and not a NaN).
			if (typeof arg !== 'number' || arg < 0 || arg !== arg)
				throw new TypeError('"defaultMaxListeners" must be a positive number');
			defaultMaxListeners = arg;
		}
	});
} else {
	EventEmitter.defaultMaxListeners = defaultMaxListeners;
}

function $getMaxListeners(that) {
	if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners;
	return that._maxListeners;
}

// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(handler, isFn, self) {
	if (isFn) handler.call(self);
	else {
		var len = handler.length;
		var listeners = arrayClone(handler, len);
		for (var i = 0; i < len; ++i) {
			try {
				listeners[i].call(self);
			} catch (e) {
				console.error(e);
			}
		}
	}
}

function emitOne(handler, isFn, self, arg1) {
	if (isFn) handler.call(self, arg1);
	else {
		var len = handler.length;
		var listeners = arrayClone(handler, len);
		for (var i = 0; i < len; ++i) {
			try {
				listeners[i].call(self, arg1);
			} catch (e) {
				console.error(e);
			}
		}
	}
}

function emitTwo(handler, isFn, self, arg1, arg2) {
	if (isFn) handler.call(self, arg1, arg2);
	else {
		var len = handler.length;
		var listeners = arrayClone(handler, len);
		for (var i = 0; i < len; ++i) {
			try {
				listeners[i].call(self, arg1, arg2);
			} catch (e) {
				console.error(e);
			}
		}
	}
}

function emitThree(handler, isFn, self, arg1, arg2, arg3) {
	if (isFn) handler.call(self, arg1, arg2, arg3);
	else {
		var len = handler.length;
		var listeners = arrayClone(handler, len);
		for (var i = 0; i < len; ++i) {
			try {
				listeners[i].call(self, arg1, arg2, arg3);
			} catch (e) {
				console.error(e);
			}
		}
	}
}

function emitMany(handler, isFn, self, args) {
	if (isFn) handler.apply(self, args);
	else {
		var len = handler.length;
		var listeners = arrayClone(handler, len);
		for (var i = 0; i < len; ++i) {
			try {
				listeners[i].apply(self, args);
			} catch (e) {
				console.error(e);
			}
		}
	}
}

function _addListener(target, type, listener) {
	var m;
	var events;
	var existing;

	if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function');

	events = target._events;
	if (!events) {
		events = target._events = objectCreate(null);
		target._eventsCount = 0;
	} else {
		// To avoid recursion in the case that type === "newListener"! Before
		// adding it to the listeners, first emit "newListener".
		if (events.newListener) {
			target.emit('newListener', type, listener.listener ? listener.listener : listener);

			// Re-assign `events` because a newListener handler could have caused the
			// this._events to be assigned to a new object
			events = target._events;
		}
		existing = events[type];
	}

	if (!existing) {
		// Optimize the case of one listener. Don't need the extra array object.
		existing = events[type] = listener;
		++target._eventsCount;
	} else {
		if (typeof existing === 'function') {
			// Adding the second element, need to change to array.
			existing = events[type] = [existing, listener];
		} else {
			existing.push(listener);
		}

		// Check for listener leak
		if (!existing.warned) {
			m = $getMaxListeners(target);
			if (m && m > 0 && existing.length > m) {
				existing.warned = true;
				var w = new Error(
					'Possible Dispatcher memory leak detected. ' +
						existing.length +
						' "' +
						String(type) +
						'" listeners ' +
						'added. Use emitter.setMaxListeners() to ' +
						'increase limit.'
				);
				w.name = 'MaxListenersExceededWarning';
				w.emitter = target;
				w.type = type;
				w.count = existing.length;
				if (typeof console === 'object' && console.warn) {
					console.warn('%s: %s', w.name, w.message);
				}
			}
		}
	}

	return target;
}

function _removeListener(type, listener) {
	var list, events, position, i, originalListener;

	if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function');

	events = this._events;
	if (!events) return this;

	list = events[type];
	if (!list) return this;

	if (list === listener || list.listener === listener) {
		if (--this._eventsCount === 0) this._events = objectCreate(null);
		else {
			delete events[type];
			if (events.off) this.emit('off', type, list.listener || listener);
		}
	} else if (typeof list !== 'function') {
		position = -1;

		for (i = list.length - 1; i >= 0; i--) {
			if (list[i] === listener || list[i].listener === listener) {
				originalListener = list[i].listener;
				position = i;
				break;
			}
		}

		if (position < 0) return this;

		if (position === 0) list.shift();
		else spliceOne(list, position);

		if (list.length === 1) events[type] = list[0];

		if (events.off) this.emit('off', type, originalListener || listener);
	}

	return this;
}

function onceWrapper() {
	if (!this.fired) {
		this.target.off(this.type, this.wrapFn);
		this.fired = true;
		switch (arguments.length) {
			case 0:
				return this.listener.call(this.target);
			case 1:
				return this.listener.call(this.target, arguments[0]);
			case 2:
				return this.listener.call(this.target, arguments[0], arguments[1]);
			case 3:
				return this.listener.call(this.target, arguments[0], arguments[1], arguments[2]);
			default:
				var args = new Array(arguments.length);
				for (var i = 0; i < args.length; ++i) args[i] = arguments[i];
				this.listener.apply(this.target, args);
		}
	}
}

function _onceWrap(target, type, listener) {
	var state = {fired: false, wrapFn: undefined, target: target, type: type, listener: listener};
	var wrapped = bind.call(onceWrapper, state);
	wrapped.listener = listener;
	state.wrapFn = wrapped;
	return wrapped;
}

function _listeners(target, type, unwrap) {
	var events = target._events;

	if (!events) return [];

	var evlistener = events[type];
	if (!evlistener) return [];

	if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener];

	return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
}

function listenerCount(type) {
	var events = this._events;

	if (events) {
		var evlistener = events[type];

		if (typeof evlistener === 'function') {
			return 1;
		} else if (evlistener) {
			return evlistener.length;
		}
	}

	return 0;
}

// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
	for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k];
	list.pop();
}

function arrayClone(arr, n) {
	var copy = new Array(n);
	for (var i = 0; i < n; ++i) copy[i] = arr[i];
	return copy;
}

function unwrapListeners(arr) {
	var ret = new Array(arr.length);
	for (var i = 0; i < ret.length; ++i) {
		ret[i] = arr[i].listener || arr[i];
	}
	return ret;
}

function objectCreatePolyfill(proto) {
	var F = function() {};
	F.prototype = proto;
	return new F();
}

function objectKeysPolyfill(obj) {
	var keys = [];
	for (var k in obj)
		if (Object.prototype.hasOwnProperty.call(obj, k)) {
			keys.push(k);
		}
	return k;
}

function functionBindPolyfill(context) {
	var fn = this;
	return function() {
		return fn.apply(context, arguments);
	};
}
